#!/usr/bin/python3
'''
这个文件是服务器的主要逻辑
一个类 CloudServer
'''
import socket
import struct
import json
import os
import threading
from umbral import *
from cloudlog import *
from clouddatabase import *

class CloudServer:
    address_family = socket.AF_INET # AF_INET IPv4因特网协议
    socket_type = socket.SOCK_STREAM # SOCK_STREAM 提供顺序的，可靠的双向的基于连接的字节流。可能支持带外数据传输机制。
    max_packet_size = 8192 # 一次性允许传输的最大字节数
    coding = 'utf-8' # 编码方式
    request_queue_size = 8 # 最大连接数
    server_dir = './data' # 服务端文件url,这里填写自己本地服务器提供的上传文件夹
    conn_list = [] # 记载成功登录的连接
    account_dict = {} # 根据连接来确定账号字典
    user_operates = ['user_upload', 'user_download', 'user_delete','user_updatedir', 'user_share', 'user_getshare']
    login_account = ['dustbird', 'alice', 'bob']
    login_password = {'dustbird':'123', 'alice':'123', 'bob':'123'}

    def __init__(self, server_address, bind_and_activate=True):
        self.server_address = server_address
        self.socket = socket.socket(self.address_family, self.socket_type)
        self.cdb = CloudDB()
        if not os.path.exists(CloudServer.server_dir):  #判断是否存在data文件夹如果不存在则创建
            os.mkdir(server_dir)
        if bind_and_activate:
            try:
                self.socket.bind(self.server_address) # 绑定套接字
                self.socket.listen(self.request_queue_size) # 监听
            except Exception as e:
                self.socket.close() # 关闭服务器套接字
                self.cdb.db_close() # 关闭数据库连接
                raise e

    def manage_connections(self):
        '''
        管理连接，每接收一个连接，就建立一个处理线程用来处理该连接的请求
        '''
        while(True):
            self.conn, self.client_addr = self.socket.accept() # 接收客户端请求conn表示的是用户的socket，注意和connet函数区分
            cloud_print('connection from {}'.format(self.client_addr)) # addr是一个ip和port的元组
            try:
                process_thd = threading.Thread(target=self.manage_user_operates, args=(self.conn, self.client_addr))
                process_thd.setDaemon(True) # 主线程退出则子线程也随之退出
                process_thd.name = str(self.client_addr[0]) + ':' + str(self.client_addr[1]) # 以str(ip:port)作为线程名
                process_thd.start()
            except Exception as e: # (ConnectionResetError)
                cloud_print('Exception in manage_connections {}'.format(repr(e))) # 打印异常信息
                self.socket.close()
                self.cdb.db_close() # 关闭数据库连接
                break
            
    def manage_user_operates(self, conn, client_addr):
        '''
        管理各种操作处理用户的请求
        注意self.conn是变化的，故这里需要保留每次不同的连接
        '''
        while(True):
            try:
                buffer = conn.recv(4)  # 获取操作码
                if not buffer: # 可以判断是否连接关闭
                    if conn in CloudServer.conn_list:
                        CloudServer.conn_list.remove(conn)
                        user = CloudServer.account_dict.pop(conn)
                    cloud_print('{} exit and {} connection disconnect.'.format(user, client_addr)) # addr是一个ip和port的元组
                    conn.close()
                    # 不判断对方是否关闭也可以，通过unpack触发异常，这样进入异常处理也ok。但我还是用了上述方法。
                    break
                code = struct.unpack('i', buffer)[0]
                if code == 1:
                    self.user_register(conn, client_addr)   
                elif code == 2:
                    self.user_login(conn, client_addr) # 登录成功则返回账号
                elif conn in CloudServer.conn_list: # 如果不是login和register，且该连接已登录，则进行其它操作
                    if code >= 3 and code <=8: # 如果有其它对应的操作则执行其它操作
                        func = getattr(self, CloudServer.user_operates[code-3])
                        func(conn, client_addr)
                    else:
                        self.reply_message(conn, -1, '操作码错误')
                        continue
                else:
                    self.reply_message(conn, -1, '该连接尚未登录')
                    raise
            except Exception as e: # 捕获异常，将conn_list和acount_dict中的连接remove
                if conn in CloudServer.conn_list:
                    CloudServer.conn_list.remove(conn)
                    user = CloudServer.account_dict.pop(conn)
                cloud_print('Exception in manage_user_operates {}'.format(repr(e))) 
                cloud_print('{} exit and {} connection disconnect.'.format(user, client_addr)) # addr是一个ip和port的元组
                conn.close()
                break
        
    def user_login(self, *args):
        conn = args[0]
        client_addr = args[1]
        head_content = self.util_unpackhead(conn) # 这里有个非常坑的点，无论信息有用与否，必须用recv把后面的信息给接收了，要不然下次读opcode时，它就读到无用的信息
        if conn in CloudServer.conn_list:
            self.reply_message(conn, -1, '该连接已经登录')    
        else:        
            if 'account' in head_content and 'password' in head_content:
                if head_content['account'] in CloudServer.login_account and head_content['password'] == CloudServer.login_password[head_content['account']]:
                    CloudServer.conn_list.append(conn)
                    CloudServer.account_dict[conn] = head_content['account']
                    
                    # 返回状态码和目录
                    file_src = os.path.normpath(os.path.join(CloudServer.server_dir, CloudServer.account_dict[conn], 'directory.txt'))
                    self.reply_message(conn, 1, '')
                    filesize = os.path.getsize(file_src)
                    file_info = {'filesize': filesize}
                    head = CloudServer.util_packhead(file_info)
                    conn.send(head)
                    # 发送文件内容 发完就发完了，服务器并不检验，因此并不接收返回码
                    send_size = 0
                    cloud_print('{} 目录下载 {} {} {}'.format(CloudServer.account_dict[conn], client_addr, '<-----', file_src))
                    with open(file_src, 'rb') as f:
                        for line in f:
                            conn.send(line)
                            send_size += len(line)
                    cloud_print('{} 登录成功 {}'.format(CloudServer.account_dict[conn], client_addr))
                else:
                    self.reply_message(conn, -1, '用户名或密码错误')
            else:
                self.reply_message(conn, -1, '登录信息出错')
        
    def user_register(self, *args):
        conn = args[0]
        client_addr = args[1]
        head = self.util_unpackhead(conn)
        self.reply_message(conn, -1, 'user_register.')
    
    @staticmethod
    def util_unpackhead(conn):
        '''
        工具函数，获取协议头部信息，为了保证协议的规范性，保证头部都是json化的字典
        '''
        head_len = struct.unpack('i', conn.recv(4))[0]
        head_json = conn.recv(head_len).decode(CloudServer.coding)
        head_content = json.loads(head_json)
        return head_content
    
    @staticmethod
    def util_packhead(head_content):
        '''
        工具函数，pack协议头部信息，为了保证协议的规范性，保证head是字典
        返回封装后的字节流，前面是内容长度后面是内容
        '''
        head_json = json.dumps(head_content)
        head_bytes = bytes(head_json, encoding=CloudServer.coding)
        return struct.pack('i', len(head_bytes)) + head_bytes

    def user_upload(self, *args):
        conn = args[0]
        client_addr = args[1]
        head_content = self.util_unpackhead(conn)
        if 'filename' in head_content and 'filesize' in head_content:
            file_path = os.path.normpath(os.path.join(CloudServer.server_dir, CloudServer.account_dict[conn], head_content['filename'])) # 规范path字符串形式,把目录和文件名合成一个路径
            filesize = head_content['filesize']
            recv_size = 0
            with open(file_path, 'wb') as f: 
                while recv_size < filesize:
                    recv_data = conn.recv(self.max_packet_size)
                    f.write(recv_data)
                    recv_size += len(recv_data)
                    # print('recvsize:%s filesize:%s' % (recv_size, filesize)) # 暂时不用
            self.reply_message(conn, 1, '')
            cloud_print('{} 上传成功 {} {} {}'.format(CloudServer.account_dict[conn], client_addr, '----->', file_path))
        else:
            self.reply_message(conn, -1, '文件包头信息解析错误')

    def user_download(self, *args):
        conn = args[0]
        client_addr = args[1]
        head_content = self.util_unpackhead(conn)
        if 'filename' in head_content:
            file_src = os.path.normpath(os.path.join(CloudServer.server_dir, CloudServer.account_dict[conn], head_content['filename']))
            if not os.path.isfile(file_src):  # 判断路径是否为文件
                self.reply_message(conn, -1, '请求的文件不存在')
            else:
                # 发送回复的状态码
                self.reply_message(conn, 1, '') # 返回status
                # 发送包头
                filesize = os.path.getsize(file_src)
                file_info = {'filesize': filesize}
                head = CloudServer.util_packhead(file_info)
                conn.send(head)
                # 发送文件内容 发完就发完了，服务器并不检验，因此并不接收返回码
                send_size = 0
                with open(file_src, 'rb') as f:
                    for line in f:
                        conn.send(line)
                        send_size += len(line)
                cloud_print('{} 下载成功 {} {} {}'.format(CloudServer.account_dict[conn], client_addr, '<-----', file_src))
        else:
            self.reply_message(conn, -1, '文件包头信息解析错误')
    
    def user_delete(self, *args):
        conn = args[0]
        client_addr = args[1]
        head_content = self.util_unpackhead(conn)
        if 'filename' in head_content:
            file_path = os.path.normpath(os.path.join(CloudServer.server_dir, CloudServer.account_dict[conn], head_content['filename']))
            if not os.path.isfile(file_path):  # 判断路径是否为文件
                self.reply_message(conn, -1, '请求的文件不存在')
            else:  
                os.remove(file_path) # 删除文件
                self.cdb.db_delete_kfrag(CloudServer.account_dict[conn], head_content['filename'])
                self.reply_message(conn, 1, '') # 返回status
                cloud_print('{} 删除成功 {} {} {}'.format(CloudServer.account_dict[conn], client_addr, 'delete', file_path))
        else:
            self.reply_message(conn, -1, '文件包头信息解析错误')
    
    def user_updatedir(self, *args):
        conn = args[0]
        client_addr = args[1]
        head_content = self.util_unpackhead(conn)
        if 'filesize' in head_content:
            # 保存公钥
            file_path = os.path.normpath(os.path.join(CloudServer.server_dir, CloudServer.account_dict[conn], 'publickey.txt'))
            with open(file_path, 'wb') as f:
                recv_data = conn.recv(33)
                f.write(recv_data)
            
            # 保存目录
            file_path2 = os.path.normpath(os.path.join(CloudServer.server_dir, CloudServer.account_dict[conn], 'directory.txt'))
            recv_size = 0
            filesize = head_content['filesize']-33
            with open(file_path2, 'wb') as f: 
                while recv_size < filesize:
                    recv_data = conn.recv(self.max_packet_size)
                    f.write(recv_data)
                    recv_size += len(recv_data)
                    # print('recvsize:%s filesize:%s' % (recv_size, filesize)) # 暂时不用
            self.reply_message(conn, 1, '')
            cloud_print('{} 更新目录 {} {} {},{}'.format(CloudServer.account_dict[conn], client_addr, '----->', file_path, file_path2))
        else:
            self.reply_message(conn, -1, '文件包头信息解析错误')
    
    def user_share(self, *args):
        conn = args[0]
        client_addr = args[1]
        head_content = self.util_unpackhead(conn)
        if 'account' in head_content and 'filename' in head_content:
            account = head_content['account']
            filename = head_content['filename']
            saccount = CloudServer.account_dict[conn]
            if account in CloudServer.login_account:
                kfrags = self.cdb.db_select_kfrag(saccount, account, filename)
                if len(kfrags) == 0:
                    # 将被分享用户的公钥发送给分享用户
                    publickey_path = os.path.normpath(os.path.join(CloudServer.server_dir, account, 'publickey.txt'))
                    with open(publickey_path, 'rb') as f:
                        publickey_bytes = f.read()
                    self.reply_messageb(conn, 1, publickey_bytes)

                    # 接收分享者发过来的kfrag_bytes
                    kfrag_content = self.util_unpackhead(conn)
                    kfrag_bytes = conn.recv(kfrag_content['kfragsize'])

                    # 存储到数据库
                    self.cdb.db_insert_kfrag(CloudServer.account_dict[conn], account, filename, kfrag_bytes)
                    self.reply_message(conn, 1, '')
                    cloud_print('{} 分享成功 {} {} {} {}'.format(saccount, client_addr, filename, '----->', account))
                else:
                    self.reply_message(conn, -1, '该文件已被分享给该用户')
            else:
                self.reply_message(conn, -1, '没有找到该账号')
        else:
            self.reply_message(conn, -1, '文件包头信息解析错误')
            
    def user_getshare(self, *args):
        
        conn = args[0]
        client_addr = args[1]
        head_content = self.util_unpackhead(conn)
        if 'account' in head_content and 'filename' in head_content:
            saccount = head_content['account']
            filename = head_content['filename']
            account = CloudServer.account_dict[conn]
            print('saccount',saccount, filename, account)
            if saccount in CloudServer.login_account:
                kfrags = self.cdb.db_select_kfrag(saccount, account, filename)
                print('kfrags',len(kfrags))
                if len(kfrags) != 0:
                    # 发送状态码
                    self.reply_message(conn, 1, '') 
                    
                    # 将分享用户的公钥发送给被分享用户
                    publickey_path = os.path.normpath(os.path.join(CloudServer.server_dir, saccount, 'publickey.txt'))
                    with open(publickey_path, 'rb') as f:
                        publickey_bytes = f.read() 
                    conn.send(publickey_bytes)
                    
                    # 发送kfrag_bytes
                    kfrag_bytes = kfrags[0][0]
                    conn.send(kfrag_bytes)
                    
                    # 发送加密后的文件
                    file_path = os.path.normpath(os.path.join(CloudServer.server_dir, saccount, filename))
                    filesize = os.path.getsize(file_path)
                    file_info = {'filesize': filesize}
                    head = CloudServer.util_packhead(file_info)
                    conn.send(head)
                    send_size = 0
                    #  发送加密后的文件
                    with open(file_path, 'rb') as f:
                        for line in f:
                            conn.send(line)
                            send_size += len(line)
                    
                    cloud_print('{} 获取分享 {} {} {}/{}'.format(account, client_addr, '<-----', saccount, filename))
                else:
                    self.reply_message(conn, -1, '该用户的该文件没有分享给您')
            else:
                self.reply_message(conn, -1, '没有找到分享链接中的账号')
        else:
            self.reply_message(conn, -1, '文件包头信息解析错误')
        
        
               

    def reply_message(self, conn, status, message):
        '''
        与reply_messageb()配对，但输入的message是str形式
        如果是1则正确，如果是-1则客户端请求错误
        '''
        status_bytes = struct.pack('i', status)
        if status == 1: # 如果为1则意味着成功，也就不返回错误信息
            reply = status_bytes
        else:
            message_bytes = message.encode(CloudServer.coding)
            head_bytes = struct.pack('i', len(message_bytes)) + message_bytes
            reply = status_bytes + head_bytes
        conn.send(reply)
    
    def reply_messageb(self, conn, status, messageb):
        '''
        与reply_message()配对，但输入的message是bytes形式
        如果是1则正确，如果是-1则客户端请求错误
        '''
        status_bytes = struct.pack('i', status)
        if status == 1: # 这个函数无论成功与否都需要返回信息，作为中间过渡函数
            if len(messageb) != 0:
                message_bytes = messageb # Here is difference
                head_bytes = struct.pack('i', len(message_bytes)) + message_bytes
                reply = status_bytes + head_bytes
            else:
                reply = status_bytes
        else:
            message_bytes = messageb # Here is difference
            head_bytes = struct.pack('i', len(message_bytes)) + message_bytes
            reply = status_bytes + head_bytes
        conn.send(reply)

server = CloudServer(('192.168.59.133', 8080))
server.manage_connections()

